マルチレベルでベースパスマッピングするAPI GatewayをAWS CDKで構築する
先日、API Gatewayがマルチレベルのベースマッピングに対応して、パスに「/」を複数設定できるようになりました。 詳しくは弊社ブログを御覧ください。
このAWS環境をAWS CDKで構築しようとしたところ、大いにハマったので注意点と実例を共有します。
CDKでAPI Gatewayのカスタムドメイン環境を構築する
まずはマルチレベルでないAPI Gatewayのカスタムドメイン環境の構築を考えてみます。
弊社ブログで解説されているので、合わせてこちらもご参照ください。
カスタムドメイン環境構築の前提として、ドメインの取得とRoute53でホストゾーンを登録してください。
本ブログでは便宜上、 mydomain.example.com
のドメインを持ち、Route53に登録しているものとして説明をします。
AWS CDKを使って次のようなAPI Gatewayのカスタムドメイン環境を構築していきます。
API GatewayのMockを使って、/hello にGETでアクセスしたら {"message": "Hello, API Gateway!!"}
を返すだけのシンプルなAPI Gatewayです。
これにカスタムドメインを利用して、 https://customdomainapi.mydomain.example.com/api/ でAPI Gatewayにアクセスできるように設定します。
以下がサンプルコードです。 実際に動かすときは、 <<AWSのアカウントID>> と <<Route53に登録しているドメイン名>> をご自身のものに書き換えてください。
#!/usr/bin/env node import "source-map-support/register"; import * as core from "@aws-cdk/core"; import * as route53 from "@aws-cdk/aws-route53"; import * as route53Targets from "@aws-cdk/aws-route53-targets"; import * as acm from "@aws-cdk/aws-certificatemanager"; import * as agw from "@aws-cdk/aws-apigateway"; const region = 'ap-northeast-1'; const accountId = <<AWSのアカウントID>>; const domainName = <<Route53に登録しているドメイン名>>; const hostName = 'customdomainapi'; const apiDomain = `${hostName}.${domainName}`; class TestStack extends core.Stack { constructor(scope: core.Construct, id: string, props?: core.StackProps) { super(scope, id, props); // Route53のホストゾーンを取得する const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { domainName: domainName, }); // AWS Certificate Managerの構築 const certificate = new acm.Certificate(this, 'Certificate', { domainName: apiDomain, validation: acm.CertificateValidation.fromDns(hostedZone), }); // API Gatewayの作成 const api = new agw.RestApi(this, 'RestApi', { restApiName: 'Rest API Sample', endpointTypes: [ agw.EndpointType.REGIONAL, ], }); // helloメソッドの作成 const method = api.root.addResource('hello').addMethod('GET', new agw.MockIntegration({ integrationResponses: [{ statusCode: '200', responseTemplates: {'application/json': '{"message": "Hello, API Gateway!!"}'} }], passthroughBehavior: agw.PassthroughBehavior.NEVER, requestTemplates: { 'application/json': '{ "statusCode": 200 }', }, }), { methodResponses: [{ statusCode: '200' }], }); // API Gatewayのカスタムドメインの設定 const domainNameApi = new agw.DomainName(this, 'CustomDomain', { certificate: certificate, domainName: apiDomain, endpointType: agw.EndpointType.REGIONAL, securityPolicy: agw.SecurityPolicy.TLS_1_2, }); // ベースパスマッピングの設定 domainNameApi.addBasePathMapping( api, { basePath: 'api', }); // Route53にApiGatewayのエイリアスレコードを設定する const apiRecord = new route53.ARecord(this, 'ApiRecord', { zone: hostedZone, target: route53.RecordTarget.fromAlias( new route53Targets.ApiGatewayDomain(domainNameApi) ), recordName: hostName, }); } } const env: core.Environment = { region: region, account: accountId, }; const app = new core.App(); new TestStack(app, 'TestStack', {env: env});
CDKで環境構築ができたら、次のコマンドを実行して動作確認します。
$ MY_DOMAIN=mydomain.example.com $ curl "https://customdomainapi.${MY_DOMAIN}/api/hello" {"message": "Hello, API Gateway!!"}
マルチレベルでないAPI Gatewayのカスタムドメイン環境の構築は問題なくできそうです。
CDKでAPI Gatewayのマルチレベルなカスタムドメイン環境を構築する(NGパターン)
同じようにマルチレベルでベースマッピングするカスタムドメイン環境の構築を考えてみます。
API GatewayのMockを使って、/hello にGETでアクセスしたら {"message": "Hello, API Gateway!!"}
を返すだけのシンプルなAPI Gatewayを構築します。
これにカスタムドメインを利用して、 https://customdomainapi.mydomain.example.com/api/a/b/c/ でAPI Gatewayにアクセスできるように設定します。
サンプルコードは次のようになるでしょう。
#!/usr/bin/env node import "source-map-support/register"; import * as core from "@aws-cdk/core"; import * as route53 from "@aws-cdk/aws-route53"; import * as route53Targets from "@aws-cdk/aws-route53-targets"; import * as acm from "@aws-cdk/aws-certificatemanager"; import * as agw from "@aws-cdk/aws-apigateway"; const region = 'ap-northeast-1'; const accountId = <<AWSのアカウントID>>; const domainName = <<Route53に登録しているドメイン名>>; const hostName = 'customdomainapi'; const apiDomain = `${hostName}.${domainName}`; class TestStack extends core.Stack { constructor(scope: core.Construct, id: string, props?: core.StackProps) { super(scope, id, props); // Route53のホストゾーンを取得する const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { domainName: domainName, }); // AWS Certificate Managerの構築 const certificate = new acm.Certificate(this, 'Certificate', { domainName: apiDomain, validation: acm.CertificateValidation.fromDns(hostedZone), }); // API Gatewayの作成 const api = new agw.RestApi(this, 'RestApi', { restApiName: 'Rest API Sample', endpointTypes: [ agw.EndpointType.REGIONAL, ], }); // helloメソッドの作成 const method = api.root.addResource('hello').addMethod('GET', new agw.MockIntegration({ integrationResponses: [{ statusCode: '200', responseTemplates: {'application/json': '{"message": "Hello, API Gateway!!"}'} }], passthroughBehavior: agw.PassthroughBehavior.NEVER, requestTemplates: { 'application/json': '{ "statusCode": 200 }', }, }), { methodResponses: [{ statusCode: '200' }], }); // API Gatewayのカスタムドメインの設定 const domainNameApi = new agw.DomainName(this, 'CustomDomain', { certificate: certificate, domainName: apiDomain, endpointType: agw.EndpointType.REGIONAL, securityPolicy: agw.SecurityPolicy.TLS_1_2, }); // ベースパスマッピングの設定 domainNameApi.addBasePathMapping( api, { basePath: 'api/a/b/c', }); // Route53にApiGatewayのエイリアスレコードを設定する const apiRecord = new route53.ARecord(this, 'ApiRecord', { zone: hostedZone, target: route53.RecordTarget.fromAlias( new route53Targets.ApiGatewayDomain(domainNameApi) ), recordName: hostName, }); } } const env: core.Environment = { region: region, account: accountId, }; const app = new core.App(); new TestStack(app, 'TestStack', {env: env});
しかし、このCDKをデプロイしようとすると、次のようなエラーが発生します。
addBasePathMapping
はマルチレベルなベースパスマッピングに対応していないようです。
$ cdk deploy TestStack Error: A base path may only contain letters, numbers, and one of "$-_.+!*'()", received: api/a/b/c
CDKでAPI Gatewayのマルチレベルなカスタムドメイン環境を構築する(OKパターン)
前述の問題は、 @aws-cdk/aws-apigatewayv2
モジュールを使うことで解決しました。
API Gatewayでマルチレベルなベースマッピングするカスタムドメイン環境を構築する際は、
@aws-cdk/aws-apigatewayv2モジュールの CfnApiMapping
を利用します。
具体的にCDKのソースコードにすると、次のような感じです。
#!/usr/bin/env node import "source-map-support/register"; import * as core from "@aws-cdk/core"; import * as route53 from "@aws-cdk/aws-route53"; import * as route53Targets from "@aws-cdk/aws-route53-targets"; import * as acm from "@aws-cdk/aws-certificatemanager"; import * as agw from "@aws-cdk/aws-apigateway"; import * as agwv2 from "@aws-cdk/aws-apigatewayv2"; const region = 'ap-northeast-1'; const accountId = <<AWSのアカウントID>>; const domainName = <<Route53に登録しているドメイン名>>; const hostName = 'customdomainapi'; const apiDomain = `${hostName}.${domainName}`; class TestStack extends core.Stack { constructor(scope: core.Construct, id: string, props?: core.StackProps) { super(scope, id, props); // Route53のホストゾーンを取得する const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', { domainName: domainName, }); // AWS Certificate Managerの構築 const certificate = new acm.Certificate(this, 'Certificate', { domainName: apiDomain, validation: acm.CertificateValidation.fromDns(hostedZone), }); // API Gatewayの作成 const api = new agw.RestApi(this, 'RestApi', { restApiName: 'Rest API Sample', endpointTypes: [ agw.EndpointType.REGIONAL, ], }); // helloメソッドの作成 const method = api.root.addResource('hello').addMethod('GET', new agw.MockIntegration({ integrationResponses: [{ statusCode: '200', responseTemplates: {'application/json': '{"message": "Hello, API Gateway!!"}'} }], passthroughBehavior: agw.PassthroughBehavior.NEVER, requestTemplates: { 'application/json': '{ "statusCode": 200 }', }, }), { methodResponses: [{ statusCode: '200' }], }); // API Gatewayのカスタムドメインの設定 const domainNameApi = new agw.DomainName(this, 'CustomDomain', { certificate: certificate, domainName: apiDomain, endpointType: agw.EndpointType.REGIONAL, securityPolicy: agw.SecurityPolicy.TLS_1_2, }); // ベースパスマッピングの設定 // domainNameApi.addBasePathMapping( // api, // { // basePath: 'api/a/b/c', // }); const cfnApiMapping = new agwv2.CfnApiMapping(this, 'ApiMapping', { apiId: api.restApiId, domainName: apiDomain, stage: 'prod', apiMappingKey: 'api/a/b/c', }); // カスタムドメイン作成後にベースパスマッピングを設定して欲しいので、dependsOn属性を付与する const cfnDomainName: agw.CfnDomainName = (domainNameApi.node.defaultChild as agw.CfnDomainName); cfnApiMapping.addDependsOn(cfnDomainName); // ステージデプロイ後にベースパスマッピングを設定して欲しいので、dependsOn属性を付与する const stage = api.node.children.find( (child) => child.node.id == 'DeploymentStage.prod' ) as core.Construct; const cfnStage = stage.node.defaultChild as agw.CfnStage; const deployment = api.node.children.find( (child) => child.node.id == 'Deployment' ) as core.Construct; const cfnDeployment = deployment.node.defaultChild as agw.CfnDeployment; cfnApiMapping.addDependsOn(cfnStage); cfnApiMapping.addDependsOn(cfnDeployment); // Route53にApiGatewayのエイリアスレコードを設定する const apiRecord = new route53.ARecord(this, 'ApiRecord', { zone: hostedZone, target: route53.RecordTarget.fromAlias( new route53Targets.ApiGatewayDomain(domainNameApi) ), recordName: hostName, }); } } const env: core.Environment = { region: region, account: accountId, }; const app = new core.App(); new TestStack(app, 'TestStack', {env: env});
このcdkソースコードであれば、マルチレベルなカスタムドメイン環境がデプロイできます。
次のコマンドを実行して動作確認すれば、問題なく構築できていることがわかります。
$ MY_DOMAIN=mydomain.example.com $ curl "https://customdomainapi.${MY_DOMAIN}/api/a/b/c/hello" {"message": "Hello, API Gateway!!"}
終わりに
AWS CDKで、API Gatewayでマルチレベルなベースパスマッピングしたカスタムドメイン環境を構築する方法を紹介しました。
私はこの環境をAWS CDKで作るにあたって大いにハマりました。
@aws-cdk/aws-apigateway
モジュールで構築するREST API環境でも、マルチレベルなベースパスマッピングをする場合は、
@aws-cdk/aws-apigatewayv2
モジュールを使わないとうまく行かない。というところに気がつくまで結構時間がかかりました。
本ブログが、同じようなことをしようとしている方の参考になれば幸いです。